//
//  GSSCommunicator.m
//  iGSS
//
//  Copyright 2010 Electronic Business Systems Ltd.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

#import "GSSCommunicator.h"

#import "NSString+GSSAdditions.h"
#import "GSSOperation.h"
#import "NetworkStatus.h"
#import "JSON.h"

#include <sys/xattr.h>

#define CACHE_DELETION_INTERVAL 604800
#define OBJECT_CACHE_SIZE 2000
#define PREFETCH_QUEUE_SIZE 10

NSString *GSSRootURI = @"https://pithos.grnet.gr/pithos";
NSString *GSSFileSearchURI = @"https://pithos.grnet.gr/pithos/rest/search";
NSString *GSSUserSearchURI = @"https://pithos.grnet.gr/pithos/rest/users";

NSString *GSSCachePath = @"~/Library/Caches";
NSString *GSSMetaCachePath = @"~/Library/Caches/Meta";
NSString *GSSDataCachePath = @"~/Library/Caches/Data";

NSString *GSSUsernameDefaultsKey = @"username";
NSString *GSSTokenDefaultsKey = @"token";


@implementation GSSCommunicator

//MARK: -
//MARK: Singleton

static GSSCommunicator *defaultCommunicator = nil;

+ (GSSCommunicator *)defaultCommunicator {
	@synchronized(self) {
		if (!defaultCommunicator)
			[[self alloc] init];	// Assignment not done here
	}
	return defaultCommunicator;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (!defaultCommunicator) {
            defaultCommunicator = [super allocWithZone:zone];
            return defaultCommunicator;
        }
    }
    return nil;
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;	// Can not be released
}

- (void)release {
    // Do nothing
}

- (id)autorelease {
    return self;
}


//MARK: -
//MARK: Instance Methods

- (id)init {
	if (self = [super init]) {
		requestQueue = [[NSOperationQueue alloc] init];
		[requestQueue setMaxConcurrentOperationCount:1];
        pendingRequests = [[NSMutableSet alloc] init];
        objectCache = [[NSMutableDictionary alloc] initWithCapacity:OBJECT_CACHE_SIZE];
        prefetchQueue = [[NSMutableArray alloc] initWithCapacity:PREFETCH_QUEUE_SIZE];        
        
        NSFileManager *fileManager = [NSFileManager defaultManager];
		[fileManager createDirectoryAtPath:[GSSMetaCachePath stringByExpandingTildeInPath]
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:NULL];
        [fileManager createDirectoryAtPath:[GSSDataCachePath stringByExpandingTildeInPath]
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:NULL];
                
		[self performSelectorInBackground:@selector(deleteOldCacheFiles) withObject:nil];
	}
	return self;
}

- (void)dealloc {    
	[username release];
	[token release];
    [home release];
	
	[requestQueue release];
    [pendingRequests release];
    [objectCache release];
    [prefetchQueue release];
		
	[super dealloc];
}

//MARK: -
//MARK: Properties

- (NSString *)username {
	if (!username)
		username = [[[NSUserDefaults standardUserDefaults] objectForKey:GSSUsernameDefaultsKey] retain];
	return username;
}

- (void)setUsername:(NSString *)newUsername {
	if ([newUsername isEqualToString:username])
		return;
	[username release];
	username = [newUsername copy];
	if (username) {
		[[NSUserDefaults standardUserDefaults] setObject:username forKey:GSSUsernameDefaultsKey];
        self.home.uri = self.homeURI;
	} else {
		[[NSUserDefaults standardUserDefaults] removeObjectForKey:GSSUsernameDefaultsKey];
        self.home.uri = nil;
    }
}

- (NSData *)token {
	if (!token)
		token = [[[NSUserDefaults standardUserDefaults] objectForKey:GSSTokenDefaultsKey] retain];
	return token;
}

- (void)setToken:(NSData *)newToken {
	if (newToken == token)
		return;
	[token release];
	token = [newToken retain];
	if (token)
		[[NSUserDefaults standardUserDefaults] setObject:token forKey:GSSTokenDefaultsKey];
	else
		[[NSUserDefaults standardUserDefaults] removeObjectForKey:GSSTokenDefaultsKey];
}

- (NSURL *)loginURL {
    return [NSURL URLWithString:[NSString stringWithFormat:@"%@/login?next=x-gr-ebs-igss://a", GSSRootURI]];
}

- (NSString *)homeURI {
    if (self.username)
        return [NSString stringWithFormat:@"%@/rest/%@/", GSSRootURI, self.username];
    else
        return nil;
}

- (GSSHome *)home {
    if (!home)
        home = [[GSSHome alloc] initWithURI:self.homeURI];
    return home;
}

- (GSSOthers *)others {
    return (GSSOthers *)[self objectWithURI:self.home.othersURI withType:GSSObjectTypeOthers];
}

- (GSSObject *)trash {
    return [self objectWithURI:self.home.trashURI withType:GSSObjectTypeTrash];
}

- (GSSTags *)tags {
    return (GSSTags *)[self objectWithURI:self.home.tagsURI withType:GSSObjectTypeTags];
}

- (GSSGroups *)groups {
    return (GSSGroups *)[self objectWithURI:self.home.groupsURI withType:GSSObjectTypeGroups];
}


//MARK: -
//MARK: Background task

- (void)deleteOldCacheFiles {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	
	[NSThread sleepForTimeInterval:5];		// Allow the app to initialize
	
	NSFileManager *fm = [NSFileManager defaultManager];
	
    for (NSString *path in [fm enumeratorAtPath:[GSSCachePath stringByExpandingTildeInPath]]) {
        NSString *fullPath = [[GSSCachePath stringByAppendingPathComponent:path]
                              stringByExpandingTildeInPath];

        BOOL isDir;
        [fm fileExistsAtPath:fullPath isDirectory:&isDir];
        if (isDir)
            continue;
        
        NSTimeInterval timestamp = 0;
        getxattr([fullPath UTF8String], GSSAcessDateAttr, &timestamp, sizeof(timestamp), 0, 0);
        NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:timestamp];
        
        NSTimeInterval timeSinceNow = [date timeIntervalSinceNow];
		unsigned long long fileSize = [[[fm attributesOfItemAtPath:path error:NULL]
										objectForKey:NSFileSize]
									   unsignedLongLongValue];

        NSTimeInterval sizeTimeOffset = fileSize / 100;		// Bigger files get deleted earlier

		if (date && (timeSinceNow + CACHE_DELETION_INTERVAL - sizeTimeOffset < 0)) {
			[fm removeItemAtPath:fullPath error:NULL];
            [NSThread sleepForTimeInterval:0.1];
		}        
    }
	
	[pool release];
}


//MARK: -
//MARK: Public Methods

- (void)logout {
    [prefetchQueue removeAllObjects];
    [requestQueue cancelAllOperations];
    [requestQueue waitUntilAllOperationsAreFinished];
    
    self.username = nil;
    self.token = nil;
    [self.home delete];
    [objectCache removeAllObjects];
}

- (NSString *)generateUniqueName:(NSString *)name withTemplate:(NSString *)template forObject:(GSSObject *)object {
    int n = 2;
    NSSet *names = [NSSet setWithArray:[object.children valueForKeyPath:@"name"]];
    while ([names containsObject:name])
        name = [NSString stringWithFormat:template, n++];
    return name;
}

- (GSSObject *)objectWithURI:(NSString *)uri withType:(GSSObjectType)type {
    if ([uri isEqualToString:self.homeURI])
        return self.home;
    
    GSSObject *object = [objectCache objectForKey:uri];
    if (!object) {
        NSInteger count = [objectCache count];
        if (count > OBJECT_CACHE_SIZE) {
            int i = random() % count;
            NSString *key = [[objectCache allKeys] objectAtIndex:i];
            [objectCache removeObjectForKey:key];
        }
        
        switch (type) {
            case GSSObjectTypeOthers:
                object = [[GSSOthers alloc] initWithURI:uri];
                break;
            case GSSObjectTypeTags:
                object = [[GSSTags alloc] initWithURI:uri];
                break;
            case GSSObjectTypeGroups:
                object = [[GSSGroups alloc] initWithURI:uri];
                break;
            default:
                object = [[GSSObject alloc] initWithURI:uri withType:type];
                break;
        }
        
        if (object)
            [objectCache setObject:object forKey:uri];
        [object release];
    }
    return object;
}

- (void)_processPrefetchQueue {
    while ([prefetchQueue count]) {
        GSSObject *object = [[[prefetchQueue objectAtIndex:0] retain] autorelease];
        [prefetchQueue removeObjectAtIndex:0];
        if (object.isStale || object.isPartial) {
            [self updateMetaOfObject:object];
            break;
        }
    }    
}

- (void)prefetchObject:(GSSObject *)object {
    while ([prefetchQueue count] >= PREFETCH_QUEUE_SIZE)
        [prefetchQueue removeObjectAtIndex:0];
    
    if (![prefetchQueue containsObject:object])
        [prefetchQueue addObject:object];
    
    if (![[requestQueue operations] count])
        [self _processPrefetchQueue];
}

- (void)updateMetaOfObject:(GSSObject *)object {
    [self updateMetaOfObject:object withDependency:nil];
}

- (void)updateMetaOfObject:(GSSObject *)object withDependency:(NSOperation *)dep {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeFetch];
    op.method = (object.type == GSSObjectTypeFile) ? @"HEAD" : @"GET";
    if (dep)
        [op addDependency:dep];
    
    NSString *key = op.key;
    if (![pendingRequests containsObject:key]) {    
        [pendingRequests addObject:key];
        [requestQueue addOperation:op];
    }
    [op release];    
}

- (void)updateFileObject:(GSSObject *)object  {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeFetch];
    op.method = @"GET";
    NSString *key = op.key;
    
    if (![pendingRequests containsObject:key]) {    
        [pendingRequests addObject:key];
        [requestQueue addOperation:op];
    }
    [op release];    
}

- (void)setMeta:(NSDictionary *)newMeta ofObject:(GSSObject *)object {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeUpdate];
    op.method = @"POST";
    op.contentType =  @"application/xml";
    op.params = [NSDictionary dictionaryWithObject:@"" forKey:@"update"];
    op.body = [[newMeta JSONRepresentation] dataUsingEncoding:NSUTF8StringEncoding];
    [requestQueue addOperation:op];
    [op release];        
}

- (void)setName:(NSString *)name ofObject:(GSSObject *)object {
    [self setMeta:[NSDictionary dictionaryWithObject:name forKey:@"name"] ofObject:object];
}

- (void)setVersioned:(BOOL)value ofObject:(GSSObject *)object {
    [self setMeta:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:value] forKey:@"versioned"]
         ofObject:object];
}

- (void)setReadForAll:(BOOL)value ofObject:(GSSObject *)object {
    [self setMeta:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:value] forKey:@"readForAll"]
         ofObject:object];
}

- (void)setTags:(NSArray *)tags ofObject:(GSSObject *)object {
    [self setMeta:[NSDictionary dictionaryWithObject:tags forKey:@"tags"] ofObject:object];
}

- (void)setPermissions:(NSArray *)permissions ofObject:(GSSObject *)object {
    [self setMeta:[NSDictionary dictionaryWithObject:permissions forKey:@"permissions"] ofObject:object];
}

- (void)moveToTrashObject:(GSSObject *)object {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeMove];
    op.method = @"POST";
	op.params = [NSDictionary dictionaryWithObject:@"" forKey:@"trash"];
    [requestQueue addOperation:op];
    [op release];
    
    [self.trash addChild:object];    
    [object.parent removeChild:object];
    
    [self updateMetaOfObject:self.trash withDependency:op];
    [self updateMetaOfObject:object.parent withDependency:op];
}

- (void)restoreFromTrashObject:(GSSObject *)object {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeMove];
    op.method = @"POST";
	op.params = [NSDictionary dictionaryWithObject:@"" forKey:@"restore"];
    [requestQueue addOperation:op];
    [op release];
    
    [self.trash removeChild:object];    
    [object.parent removeChild:object];
    
    [self updateMetaOfObject:self.trash withDependency:op];
    [self updateMetaOfObject:object.parent withDependency:op];
}

- (void)deleteObject:(GSSObject *)object {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:object withType:GSSOperationTypeMove];
    op.method = @"DELETE";
    [requestQueue addOperation:op];
    [op release];
    
    [self.trash removeChild:object];
    [objectCache removeObjectForKey:object.uri];
    
    [self updateMetaOfObject:self.trash withDependency:op];
}

- (void)createFolderNamed:(NSString *)name inFolder:(GSSObject *)folder {
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:folder withType:GSSOperationTypeCreate];
    op.method = @"POST";
    op.params = [NSDictionary dictionaryWithObject:name forKey:@"new"];
    [requestQueue addOperation:op];
    [op release];
    
    NSString *uri = [folder.uri stringByAppendingURIComponent:name];
    GSSObject *newFolder = [self objectWithURI:uri withType:GSSObjectTypeFolder];
    newFolder.name = name;
    newFolder.uploading = YES;
    [folder addChild:newFolder];
    
    [self updateMetaOfObject:newFolder withDependency:op];
    [self updateMetaOfObject:folder withDependency:op];
}

- (void)createFile:(GSSObject *)file withData:(NSData *)data inFolder:(GSSObject *)folder {    
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:file withType:GSSOperationTypeCreate];
    op.method = @"PUT";
	op.body = data;
	op.contentType = file.content;
    [requestQueue addOperation:op];
    [op release];
    
    file.uploading = YES;
    [folder addChild:file];
    
    [self updateMetaOfObject:file withDependency:op];
    [self updateMetaOfObject:folder withDependency:op];
}


- (GSSObject *)searchFiles:(NSString *)prefix {
	NSString *enc = [prefix stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
	NSString *uri = [NSString stringWithFormat:@"%@/%@", GSSFileSearchURI, enc];
    GSSQuery *query = [[GSSQuery alloc] initWithURI:uri];
    GSSOperation *op = [[GSSOperation alloc] initWithGSSObject:query withType:GSSOperationTypeSearch];
    op.method = @"GET";
    
    NSString *key = op.key;
    if (![pendingRequests containsObject:key]) {    
        [pendingRequests addObject:key];
        [requestQueue addOperation:op];
    }
    
    [op release];
    return [query autorelease];
}


//MARK: -
//MARK: GSSOperation Callback

- (void)operationFinished:(GSSOperation *)operation {
    [pendingRequests removeObject:operation.key];
    
    NSArray *operations = [requestQueue operations];
    NSUInteger operationsCount = [operations count];
    if (!operationsCount || (operationsCount == 1 && [operations containsObject:operation])) {
        // This was the last operation
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        [self _processPrefetchQueue];
    }
}

@end